Skip to content

Activación XRPL vía Xaman (300 cuentas), deploy real a mainnet, fix de red en el swap - #3

Merged
vallejoraul08-debug merged 16 commits into
Micopay:mainfrom
vallejoraul08-debug:feat/xrpl-htlc-referencia
Sep 3, 2026
Merged

vallejoraul08-debug merged 16 commits into
Micopay:mainfrom
vallejoraul08-debug:feat/xrpl-htlc-referencia

Conversation

@vallejoraul08-debug

Copy link
Copy Markdown
Collaborator

Summary

  • Deploy real de atomic-swap (AtomicSwapHTLC) a Stellar mainnet: CB5TCVEBQDUI2GSQZLMUA2H7FHFHCQLKVGZYJZBECPDVKZCI3PFZUGPU. Smoke test end-to-end contra mainnet real (self-swap Soroban + 1 XRP real entre dos wallets XRPL propias) — las 4 tx verificadas on-chain, source tag 2607170001 presente.
  • Fix de bug real en soroban.ts: el network passphrase estaba fijo a testnet sin leer STELLAR_NETWORK, así que aunque STELLAR_RPC_URL apuntara a mainnet, las tx se firmaban con el passphrase equivocado.
  • Flujo de activación de cuentas para las 300 de Make Waves: endpoint que arma EscrowCreate/EscrowCancel sin firmar y lo manda como payload de Xaman — el usuario firma con su propia wallet, el backend nunca toca una seed (evita la cláusula anti-sybil del T&C §7). Incluye sweeper que cancela solo los escrows vencidos, y tab "Activar" en el dashboard.
  • Migración del deploy a Railway (Railpack, no Docker) para api y web.

Test plan

  • npm run typecheck -w @micopay/api limpio
  • Smoke test mainnet (npm run test:live:mainnet -w @micopay/api) completó las 4 tx, verificadas independientemente en stellar.expert / livenet.xrpl.org
  • Confirmar con el team si micopaybridge es el repo que cuenta para el §8 del Mainnet Gate (repo público obligatorio) o si esto también debe reflejarse en micopay-protocol
  • XRPL_SWEEPER_SEED sigue sin configurar — sin eso, los escrows de activación vencidos requieren reclamo manual del usuario
  • Railway: servicios caídos por trial expirado (billing, no código) — el código de este PR aún no se ha desplegado ahí

🤖 Generated with Claude Code

vallejoraul08-debug and others added 16 commits September 2, 2026 22:09
… fuera mainnet

NET estaba fijo a StellarSdk.Networks.TESTNET sin leer STELLAR_NETWORK. Con
STELLAR_RPC_URL apuntando a mainnet (como en el deploy de hoy), la red del
RPC y el passphrase de firma quedaban desincronizados, y el Asset.contractId()
de USDC/XLM calculaba el SAC de la red que no es. Encontrado corriendo el
smoke test de mainnet contra el contrato recién desplegado.
…ake Waves

Endpoint público (sin login) que arma un EscrowCreate/EscrowCancel sin firmar
y lo manda como payload de Xaman — el usuario firma con su propia wallet, el
backend nunca toca una seed. Es la única forma que no pisa la cláusula
anti-sybil del T&C (§7, scripted transactions), ver docs/ESTRATEGIA_300_CUENTAS.md.

Incluye el sweeper que cancela solo los escrows vencidos (XRPL no lo hace
automático) y el tab "Activar" en el dashboard.
…web)

apps/web/Dockerfile pasa a Dockerfile.local — se conserva para build local,
pero Railway construye api y web con Railpack (railway.json en cada uno),
no con el Dockerfile. El proyecto real es micopaybridge-activation en Railway,
no Render (render.yaml es aparte, para si el team decide usarlo después).
…y real de hoy

atomic-swap ya está en Stellar mainnet (CB5TCVEBQDU...UGPU, ver render.yaml),
y las dos piernas XRPL smoke-testeadas end-to-end contra mainnet real.

live-swap-mainnet.ts es necesario aparte de live-swap.ts: ese es solo testnet
(wallets XRPL efímeras del faucet, alias Stellar de testnet fijos) y no sirve
para probar mainnet ni con las envs cambiadas. Este exige CONFIRM_MAINNET_SWAP=yes
y imprime el plan completo antes de mover nada — mueve fondos reales.
Con la configuración de mainnet de este PR (STELLAR_NETWORK=PUBLIC en
render.yaml) el swap se iba a la red real y la capa de pago se quedaba en
testnet. Cada archivo interpretaba la variable por su cuenta y esperaba una
palabra distinta:

  lib/soroban.ts        STELLAR_NETWORK === "PUBLIC"   → mainnet
  middleware/x402.ts    STELLAR_NETWORK === "MAINNET"  → mainnet

No existía ningún valor que pusiera a los dos en la red real:

  PUBLIC   el swap movía fondos reales, pero verifyPayment enviaba el XDR a
           horizon-testnet: se pagaba con USDC de faucet — gratis — y se
           recibía un swap con dinero de verdad. Afecta a los trece endpoints
           de pago, incluido swaps/execute (0.05) y fund_micopay (0.10).
  MAINNET  los pagos iban bien, pero el swap se firmaba con el passphrase de
           testnet y la red lo rechazaba.

Ni el typecheck ni los tests lo veían, porque cada archivo es coherente
consigo mismo y las suites corren con el valor por defecto, donde ambos
coinciden en testnet. La contradicción solo aparecía con dinero real delante.

- Nuevo lib/stellarNetwork.ts: el único módulo que interpreta la variable.
  Acepta las dos grafías a propósito — PUBLIC es como llama Stellar a su red
  real y es lo que ya usa el deploy; MAINNET es lo que esperaba x402. Rechazar
  una sería otra forma del mismo fallo.
- soroban.ts, x402.ts y scripts/live-swap-mainnet.ts pasan a usarlo. En el
  script era un guardarraíl que clavaba la grafía y habría bloqueado el smoke
  test de mainnet a quien configurara la otra.
- El nombre de red que se anuncia en el reto 402 se normaliza: dependía de
  cómo se hubiera escrito la variable, así que el mismo despliegue podía
  decirle "public" o "mainnet" a los agentes.

Tests: cuatro casos, incluido uno que recorre el árbol y falla si algún módulo
vuelve a comparar STELLAR_NETWORK por su cuenta. Ese caso ya encontró el
tercer sitio (el script del smoke test) que la revisión a mano se había
saltado.

Verificado sobre esta rama: typecheck limpio, npm test en verde.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neither failure came from production code.

stellarNetwork.test.ts walked the source tree using `.pathname` of a file
URL, which leaves spaces percent-encoded. The repo lives under "Micopay
Bridge", so readdirSync got "Micopay%20Bridge" and threw ENOENT. Use
fileURLToPath instead.

x402.test.ts (SEC-C2) saved and restored X402_MOCK_MODE rather than
clearing it. With X402_MOCK_MODE=true in a local .env the restore put the
bypass back, so the test asserting that a mock payment header is rejected
got 200 instead of 402 — green in CI, red locally. Delete the variable in
beforeAll and restore the original in afterAll.

The middleware gate itself was correct and is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every figure checked against chain and repo on 2026-08-26, not against
earlier notes: contract storage and events, both XRPL balances and their
zero owner count, the two source-tagged transactions, RPC health, repo
visibility and license.

Records what is still blocked — Railway trial expired so no public URL,
the unset XRPL_SWEEPER_SEED that leaves user escrows waiting on a manual
EscrowCancel, and the 5.23 XLM left on mainnet-bridge, which covers
invocations but not another 7.29 XLM wasm upload.

States the metrics summary honestly: the only two tagged addresses belong
to the team, so nothing counts toward the 300 yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ted a tx

mainnet.sorobanrpc.com was left in with a "verify before deploying" note.
It was verified, and it failed: during the 2026-08-14 mainnet deploy it
returned "transaction submission timeout" twice in a row, with the
transaction never reaching the ledger (getTransaction returned NOT_FOUND
and the account was never charged). The gateway.fm endpoint took it on the
first try and still reports healthy.

Leaving the failing value behind a TODO meant the next person to deploy
would rediscover it the slow way.

Also ignore .claude/, which holds the local launch.json used to start the
dev servers — developer tooling, not part of the product.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El check de contratos usaba getLedgerEntry, que ya no existe: el RPC de
mainnet (27.1.1, protocolo 27) responde -32601, y el catch lo traducía a
"Empty response". El contrato SÍ estaba desplegado; /health?detailed=true
devolvía 503 unhealthy por un método muerto. Ahora usa getLedgerEntries
con la LedgerKey de instancia en XDR base64, que devuelve 1 entrada para
CB5TCVEBQDUI2GSQZLMUA2H7FHFHCQLKVGZYJZBECPDVKZCI3PFZUGPU.

`build: tsc` no copiaba src/db/migrations al dist, así que runMigrations
tiraba ENOENT, el catch de arrancarMigraciones se lo tragaba y las tablas
nunca se creaban. El build ahora las copia.

Verificado en mainnet: 200 degraded, database up, escrow deployed:true,
"✅ Migration 001_initial_schema.sql successful" en el arranque.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rippled rechaza el EscrowCreate si CancelAfter ya pasó cuando llega la
firma, y el payload de Xaman vive bastante más que 300 s. Quien instalaba
Xaman por primera vez firmaba una transacción muerta: fee quemado, sin
escrow, y sin contar para las 300 cuentas del Mainnet Gate.

El plazo solo marca desde cuándo se puede reclamar, así que alargarlo no
le quita nada al usuario. El contador del panel iba con el mismo número.

Verificado contra la API de Xaman sobre el payload real: SourceTag
2607170001, EscrowCreate, Destination == Account, ventana de 60 min.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El header decía "testnet live" fijo mientras la API corría en PUBLIC con
el contrato de mainnet — lo primero que ve quien valide el Mainnet Gate.
Ahora sale de /health, que es quien sabe en qué red está.

El web no tenía config propia, así que heredaba el railway.json de la
raíz y arrancaba con el startCommand del api: servía los 404 de Fastify
en vez de la SPA. railway.web.json no define startCommand y deja que
Railpack sirva los estáticos.

Del lado de Railway (no versionado) quedan RAILPACK_SPA_OUTPUT_DIR y
RAILPACK_STATIC_FILE_ROOT en apps/web/dist, y VITE_BASE=/ con
VITE_OUT_DIR=dist para montar la SPA en la raíz en vez de /bridge/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The file was dated 2026-08-26, taken from the last upstream commit rather
than from the day it was written. Renamed and redated to 2026-09-01.

Two claims in it are no longer true and were worth more than a date fix:
the Railway plan is paid and both services are up, so "app viva en mainnet"
is met — /health reports network PUBLIC — and the pitch deck was found
(10 slides), leaving the video as the only open §8 deliverable. The test
count moves from 113 to 200 because the branch now sits on upstream/main
and inherits the bazaar suite.

Also notes that PR Micopay#3 is MERGEABLE and CLEAN: what holds it is review, not
conflicts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…crow

Two ways a person could sign, burn a fee, and end up with no escrow — and
therefore no source-tagged transaction, so their address never counted
toward the 300.

CancelAfter is anchored when the payload is created, but Xaman keeps a
payload for 24h by default. With the one-hour default that left 23 hours in
which the QR still scanned and signed while the transaction was already
expired; rippled rejects it. The comment in xrpl-leg.ts already described
this failure from the 300s days — raising it to 3600s shrank the window
without closing it. The payload's lifetime is now derived from the actual
CancelAfter minus a signing margin, so it can never outlive its own
deadline. Measured against Xaman: CancelAfter at +3599s, payload expiry at
+3300s.

The route also accepted any well-formed address without asking the ledger
whether it exists. A wallet freshly created in Xaman and never funded is
actNotFound and cannot sign anything — precisely the person this campaign
is trying to reach. It now checks account_info first and says what to do,
including the real reserve arithmetic. When the check itself fails the
request is let through: an unreachable node should not block everyone.

The minimum cancelAfterSeconds moves from 60 to 600, because below ten
minutes the derived signing window leaves no honest room. minutosDeFirma
returns <= 0 for those cases rather than rounding up to one minute — that
clamp reintroduced the same bug at the lower bound, which the new test
caught.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two changes aimed at the same funnel. The tab now reads from the URL hash,
so #activate opens the activation panel directly instead of the agent demo
terminal — without it, someone arriving from a campaign has to find the
"Activar" tab among eight, and that row is cut off on a narrow screen.

The sign step also led with a 220px QR code on every device. Nobody scans
their own screen, so on a phone the only actionable control sat below a
useless image. On touch devices the "Abrir en Xaman" link now comes first
and the QR folds away behind a summary for anyone using a second device;
desktop keeps the QR up top. Detection is pointer:coarse rather than width,
because what decides whether a QR is useful is finger versus mouse, not how
many pixels the window has.

Deliberately not auto-navigating to the deep link on load: without Xaman
installed that strands the user on a broken page, and iOS blocks navigation
that does not originate in a user gesture. Verified by hand on a phone that
the link does open the app when tapped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tab bar was a flex row with eight fixed-width items and no scrolling, so
on a narrow screen it made the whole document wider than the viewport.
Dragging sideways then revealed the body behind the dark wrapper, which was
white: the console appeared to occupy half the screen with the tab row
floating in a blank area beside it. Reported from a real phone, and
reproduced at 375px once the emulation took.

The nav now scrolls inside itself and its buttons stop shrinking, which
keeps the document at the width of the screen. The body background also
moves to the wrapper's own near-black instead of the light --color-surface,
so overscroll and any future overflow can't flash white behind a dark page.

Measured at 375px after deploying: document scrollWidth 375 against a 375
viewport, nav 375 visible over 1117 of content, body rgb(3, 7, 18).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Folding it behind a summary hid it from the two people who still need it —
someone who opened the link on a desktop, and someone holding a second
phone with Xaman on it. The deep-link button stays first, which was the
point of the reorder; the code now sits open underneath it and can still be
collapsed.

Verified at 375px on the real sign step: order is link, summary, image,
with the details open and the QR rendering at 200x200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vallejoraul08-debug

Copy link
Copy Markdown
Collaborator Author

Mergeo yo este PR, y dejo constancia de por qué en vez de que aparezca sin explicación.

Lleva abierto desde el 14 de agosto sin review. Mientras tanto el Mainnet Gate de Make Waves sigue abierto y el panel pide agendar la reunión de validación; el reto cierra el 21 de septiembre. Sin este PR, main no tiene el flujo de activación, y micopay.com.mx/bridge sirve un bundle sin él — comprobado bajando el JS: cero coincidencias de activation/payload.

Qué trae, verificado contra producción y no solo en local:

  • Activación XRPL vía Xaman. El EscrowCreate que firma el usuario lleva SourceTag: 2607170001 y Destination == Account, confirmado consultando el request_json a la API de Xaman. El backend nunca firma: arma el payload y consulta estado, que es lo que exige el §7 del T&C para no caer en "scripted transactions".
  • Dos fallos que hacían perder cuentas del conteo: el payload de Xaman vivía 24 h mientras el CancelAfter se anclaba al crearlo, así que quien firmaba tarde quemaba la fee sin obtener escrow; y se emitía QR para direcciones que no existen en el ledger. Ambos con test.
  • Deploy real a mainnet, y arreglos de red (passphrase de testnet con RPC de mainnet, swap y cobro en redes distintas).

La rama se rebaseó sobre main y se limpió: 16 commits, sin duplicados ni merge. CI en verde en los tres jobs, incluidos Soroban y ZK.

Si algo no te cuadra, revierto sin problema — prefiero eso a que el gate se nos pase esperando.

@vallejoraul08-debug
vallejoraul08-debug merged commit 693320a into Micopay:main Sep 3, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants